using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.SqlServer.Dts.Pipeline;
using Microsoft.SqlServer.Dts.Pipeline.Wrapper;
using Microsoft.SqlServer.Dts.Runtime.Wrapper;
using System.Security.Cryptography;
using System.Collections;
using System.IO;
using System.Text.RegularExpressions;
using Microsoft.SqlServer.Types;
using Microsoft.SqlServer.Dts.Runtime;
using Geospatial;

namespace MapPointBatchGeocoder
{
    /// <summary>
    /// This class represents the asynchronous SSIS transformation component for batch geocoding of address data.
    /// </summary>
    [DtsPipelineComponent(DisplayName = "MapPoint Batch Geocoder", ComponentType = ComponentType.Transform)]
    public class GeocoderTransformation : PipelineComponent
    {
        private enum CustomPropertiesBase { Literal = 0, Variables = 1, InputColumns = 2 };        // used internally by the helper function AddCustomProperty to add custom properties

        #region Constants
        private const int MAPPOINT_SRID = 4326;                                                 // Spatial Reference ID of MapPoint
        private const string MAPPOINT_ENTITYTYPENAME = "BatchGeocode";                          // name of the entity type used by MapPoint
        private const string OUTPUT_COLUMN_NAME_GEOPOINT = "GeoPoint";                          // name of the GeoPoint output column
        private const string OUTPUT_COLUMN_NAME_MATCHCODE = "MatchCode";                        // name of the MatchCode output column
        private const string PROPERTY_NAME_MP_USERNAME = "MapPoint UserName";                   // these represent the names of the associated transformation property
        private const string PROPERTY_NAME_MP_PASSWORD = "MapPoint Password";
        private const string PROPERTY_NAME_MP_USESTAGING = "MapPoint UseStaging";
        private const string PROPERTY_NAME_MP_DATASOURCENAME = "MapPoint DataSourceName";
        private const string PROPERTY_NAME_SERVICE_RETRYATTEMPTS = "Service Retry Attempts";    
        private const string PROPERTY_NAME_SERVICE_RETRYDELAY = "Service Retry Delay";
        private const string PROPERTY_NAME_SERVICE_STUBDATA = "Service Stub MapPoint Data";
        private const string PROPERTY_NAME_INPUT_ENTITYID = "Source EntityID";
        private const string PROPERTY_NAME_INPUT_ADDRESSLINE = "Source AddressLine";
        private const string PROPERTY_NAME_INPUT_POSTALCODE = "Source PostalCode";
        private const string PROPERTY_NAME_INPUT_PRIMARYCITY = "Source PrimaryCity";
        private const string MAPPOINT_RETURN_VALUE_BAD = "Bad";                                 // value returned by MapPoint indicating a bad geocode address
        private const string REGEX_MATCH = "[,<>;]";                                             // used to strip out problematic characters from the address fields
        private const string REGEX_REPLACE = " ";                                               // used to strip out problematic characters from the address fields
        #endregion

        #region Private Member Variables
        private PipelineBuffer outputBuffer;                    // transformation's output buffer for asynchronous output
        private Hashtable bufferInputLineageMap;                // used to store the lineage mapping b/w the input and output buffers
        private Hashtable bufferOutputLineageMap;               // used to store the lineage mapping b/w the input and output buffers
        private Hashtable passThruRows;                         // used to pass thru other input columns of information not directly related to geocoding, but defined in the input buffer
        private Geocoder geocoder;                              // local reference to our geocoder component
        
        // During PreExecute the transformation's properties will get resolved (from package 
        // variables, input columns, or literals) and stored in the following variables.
        private string mappointUserName = "";                   // username of the MapPoint account
        private string mappointPassword = "";                   // password of the MapPoint account
        private bool mappointUseStaging;                        // set to True to use the MapPoint staging environment
        private int serviceRetryAttempts = 10;                  // number of attempts to retry accessing MapPoint services
        private int serviceRetryDelay = 5;                      // delay between attempts
        private bool serviceStubMapPointData;                   // will cause the transformation to avoid any Web Service calls and synthesis fake lat/lon values instead
        private string mappointDataSourceName = "";             // MapPoint data source name to use
        #endregion

        /// <summary>
        /// Overrides the ProcessInput method.  See books online for more information.
        /// </summary>
        public override void ProcessInput(int inputID, PipelineBuffer buffer)
        {
            // Check to see if there are rows in the buffer.
            if (!buffer.EndOfRowset)
            {
                // Create a hashtable used to pass thru other columns of information not directly
                // related to geocoding, but defined for the row.
                passThruRows = new Hashtable();

                // Advance the buffer to the next row.
                while (buffer.NextRow())
                {
                    // Remove any commas in the source columns as they'll interfere with the comma-delimited 
                    // file format.  Would be better to use a different format, but MapPoint doesn't appear
                    // to support a different format on upload, only download.
                    string inputEntityID = Regex.Replace(buffer[(int)bufferInputLineageMap[SourceEntityIDColumnName]].ToString(), REGEX_MATCH, REGEX_REPLACE);
                    string inputAddressLine = Regex.Replace(buffer[(int)bufferInputLineageMap[SourceAddressLineColumnName]].ToString(), REGEX_MATCH, REGEX_REPLACE);
                    string inputPrimaryCity = Regex.Replace(buffer[(int)bufferInputLineageMap[SourcePrimaryCityColumnName]].ToString(), REGEX_MATCH, REGEX_REPLACE);
                    string inputPostalCode = Regex.Replace(buffer[(int)bufferInputLineageMap[SourcePostalCodeColumnName]].ToString(), REGEX_MATCH, REGEX_REPLACE);

                    // Save a copy of all the passthru values to join back to the 
                    // geocoded data and send to the output
                    Hashtable passThruValues = new Hashtable();

                    foreach (IDTSInputColumn100 col in InCols)
                    {
                        // Although the batch geocoded data includes the address information, we still add it here 
                        // so the output maintains the original, unmodified values.
                        passThruValues.Add(col.Name, buffer[(int)bufferInputLineageMap[col.Name]]);
                    }

                    if (passThruValues.ContainsKey(inputEntityID)) { throw new ApplicationException("Duplicate EntityID detected.  EntityID must be unique"); }
                    passThruRows.Add(inputEntityID, passThruValues);

                    // Add the current row to our batch buffer
                    geocoder.AddAddress(inputEntityID, inputAddressLine, inputPrimaryCity, inputPostalCode);
                }

                // Flush the current batch (causes the geocoding operation to execute)
                if (geocoder.AddressCount != 0) FlushCurrentBatch();

                passThruRows = null;
            }
            else
            {
                // EndOfRowset on the input buffer is true.
                // Set EndOfRowset on the output buffer.
                outputBuffer.SetEndOfRowset();
            }
        }

        /// <summary>
        /// Processes the batch of addresses currently added to the geocoder component, and adds the geocoded data to the output.
        /// </summary>
        private void FlushCurrentBatch()
        {
            // Use our geocoder utility to perform the geocoding process with MapPoint.
            geocoder.ExecuteBatch();

            // Read the geocoded data
            while (geocoder.Read())
            {
                // Add a row to the output buffer.
                outputBuffer.AddRow();

                // Add the passthru values back to the output by finding the correct row in the passthru collection
                Hashtable passThruValues = (Hashtable)passThruRows[geocoder.GetEntityID];      

                foreach (IDTSInputColumn100 col in InCols)
                {
                    outputBuffer[(int)bufferOutputLineageMap[col.Name]] = passThruValues[col.Name];
                }

                // Copy the data from the input buffer column to the output buffer column.
                outputBuffer[(int)bufferOutputLineageMap[OUTPUT_COLUMN_NAME_MATCHCODE]] = geocoder.GetMatchCode;

                // Verify the quality of the geocode
                if (geocoder.GetMatchCode.ToLower() == MAPPOINT_RETURN_VALUE_BAD.ToLower())
                {
                    // Bad item.  Default is to return 0, set them to Null.
                    outputBuffer.SetNull((int)bufferOutputLineageMap[OUTPUT_COLUMN_NAME_GEOPOINT]);
                }
                else
                {
                    // Good item.  Return the values.
                    outputBuffer[(int)bufferOutputLineageMap[OUTPUT_COLUMN_NAME_GEOPOINT]] = NewGeoPoint(geocoder.GetLatitude, geocoder.GetLongitude).Value;
                }
            }

            // Reset the geocoder for the next batch.
            geocoder.Reset();
        }

        /// <summary>
        /// Overrides the PreExecute method.  See books online for more information.
        /// </summary>
        public override void PreExecute()
        {
            base.PreExecute();

            // Resolve the MapPoint settings, some which come from variables.
            //
            IDTSVariables100 vars = null;
            VariableDispenser.LockForRead(ComponentMetaData.CustomPropertyCollection[PROPERTY_NAME_MP_USESTAGING].Value.ToString());
            VariableDispenser.LockForRead(ComponentMetaData.CustomPropertyCollection[PROPERTY_NAME_MP_DATASOURCENAME].Value.ToString());
            VariableDispenser.LockForRead(ComponentMetaData.CustomPropertyCollection[PROPERTY_NAME_MP_USERNAME].Value.ToString());
            VariableDispenser.LockForRead(ComponentMetaData.CustomPropertyCollection[PROPERTY_NAME_MP_PASSWORD].Value.ToString());

            VariableDispenser.GetVariables(out vars);

            mappointUserName = vars[ComponentMetaData.CustomPropertyCollection[PROPERTY_NAME_MP_USERNAME].Value].Value.ToString().Trim();
            mappointPassword = vars[ComponentMetaData.CustomPropertyCollection[PROPERTY_NAME_MP_PASSWORD].Value].Value.ToString().Trim();
            mappointUseStaging = (bool)vars[ComponentMetaData.CustomPropertyCollection[PROPERTY_NAME_MP_USESTAGING].Value].Value;
            mappointDataSourceName = vars[ComponentMetaData.CustomPropertyCollection[PROPERTY_NAME_MP_DATASOURCENAME].Value].Value.ToString().Trim();
            serviceRetryAttempts = (int)ComponentMetaData.CustomPropertyCollection[PROPERTY_NAME_SERVICE_RETRYATTEMPTS].Value;
            serviceRetryDelay = (int)ComponentMetaData.CustomPropertyCollection[PROPERTY_NAME_SERVICE_RETRYDELAY].Value;
            serviceStubMapPointData = (bool)ComponentMetaData.CustomPropertyCollection[PROPERTY_NAME_SERVICE_STUBDATA].Value;

            // For safety purposes, we do not allow stubbing data when in production
            if (serviceStubMapPointData && !mappointUseStaging) 
                { throw new ApplicationException("Cannot use the 'Stub MapPoint Data' option in the production environment."); }

            // According to documentation, the indexes of the input columns of the buffer
            // stream may not correspond to the original input columns of the metadata.  You
            // must provide a mapping between the two using FindColumnByLineageID, which is
            // what the code below does.  We will store this mapping in two separate Hashtables,
            // input and output, for lookup purposes later on.
            bufferInputLineageMap = new Hashtable();
            bufferOutputLineageMap = new Hashtable();

            foreach (IDTSInputColumn100 inCol in InCols)
            {
                bufferInputLineageMap.Add(inCol.Name, BufferManager.FindColumnByLineageID(ComponentMetaData.InputCollection[0].Buffer, inCol.LineageID));
            }
            
            foreach (IDTSOutputColumn100 outCol in OutCols)
            {
                bufferOutputLineageMap.Add(outCol.Name, BufferManager.FindColumnByLineageID(ComponentMetaData.OutputCollection[0].Buffer, outCol.LineageID));
            }
            
            // Create the batch geocoder, and initialize
            geocoder = new Geocoder(mappointUserName, mappointPassword, mappointDataSourceName, MAPPOINT_ENTITYTYPENAME, mappointUseStaging, serviceRetryAttempts, serviceRetryDelay, serviceStubMapPointData);
            geocoder.RaiseLogVerboseEvent += HandleGeocoderLogVerboseEvent;
        }

        public override void PostExecute()
        {
            base.PostExecute();

            this.geocoder.Dispose();
        }
        /// <summary>
        /// Overrides the PrimeOutput method.  See books online for more information.
        /// </summary>
        public override void PrimeOutput(int outputs, int[] outputIDs, PipelineBuffer[] buffers)
        {
            if (buffers.Length != 0) 
                outputBuffer = buffers[0];
        }

        /// <summary>
        /// Overrides the ProvideComponentProperties method.  See books online for more information.
        /// </summary>
        public override void ProvideComponentProperties()
        {
            // Let the base class add the input and output objects.
            base.ProvideComponentProperties();

            // Name the input and output, and make the output asynchronous.
            IDTSOutput100 output = ComponentMetaData.OutputCollection[0];
            IDTSInput100 input = ComponentMetaData.InputCollection[0];
            input.Name = "Input";
            output.Name = "Output";
            output.SynchronousInputID = 0;

            // Add the Custom properties for the MapPoint settings and source mappings
            //
            AddCustomProperty(PROPERTY_NAME_MP_USERNAME, "The username of the MapPoint account.", "", CustomPropertiesBase.Variables);
            AddCustomProperty(PROPERTY_NAME_MP_PASSWORD, "The password of the MapPoint account.", "", CustomPropertiesBase.Variables);
            AddCustomProperty(PROPERTY_NAME_MP_USESTAGING, "Set to True to use the MapPoint staging environment.", "", CustomPropertiesBase.Variables);
            AddCustomProperty(PROPERTY_NAME_MP_DATASOURCENAME, "The name of the data source to use for geocoding (i.e. MyAccount123456.123456.Customer). The datasource will be created if it does not exist.", "", CustomPropertiesBase.Variables);
            AddCustomProperty(PROPERTY_NAME_SERVICE_RETRYATTEMPTS, "Number of attempts to retry accessing MapPoint services in case of network failure.", serviceRetryAttempts, CustomPropertiesBase.Literal);
            AddCustomProperty(PROPERTY_NAME_SERVICE_RETRYDELAY, "The delay between retries (in seconds).", serviceRetryDelay, CustomPropertiesBase.Literal);
            AddCustomProperty(PROPERTY_NAME_SERVICE_STUBDATA, "If true, will cause the transformation to avoid any Web Service calls and instead will synthesis fake lat/lon values instead of invoking the service. Useful for testing purposes.", serviceStubMapPointData, CustomPropertiesBase.Literal);
            AddCustomProperty(PROPERTY_NAME_INPUT_ENTITYID, "The input column to use as the MapPoint EntityID.", "", CustomPropertiesBase.InputColumns);
            AddCustomProperty(PROPERTY_NAME_INPUT_ADDRESSLINE, "The input column to use as the MapPoint AddressLine.", "", CustomPropertiesBase.InputColumns);
            AddCustomProperty(PROPERTY_NAME_INPUT_PRIMARYCITY, "The input column to use as the MapPoint PrimaryCity.", "", CustomPropertiesBase.InputColumns);
            AddCustomProperty(PROPERTY_NAME_INPUT_POSTALCODE, "The input column to use as the MapPoint PostalCode.", "", CustomPropertiesBase.InputColumns);            
        }

        /// <summary>
        /// Overrides the ReinitializeMetaData method.  See books online for more information.
        /// </summary>
        public override void ReinitializeMetaData()
        {
            if (!ComponentMetaData.AreInputColumnsValid) ComponentMetaData.RemoveInvalidInputColumns();

            // Remove all existing output columns
            ComponentMetaData.OutputCollection[0].OutputColumnCollection.RemoveAll();

            // Add the output columns, making them identical to the input columns PLUS two
            // additional columns for GeoPoint (to store the geocoded value) and 
            // MatchCode (quality of the geocode).
            //
            foreach (IDTSInputColumn100 inCol in InCols)
            {
                IDTSOutputColumn100 outCol = ComponentMetaData.OutputCollection[0].OutputColumnCollection.New();
                outCol.Name = inCol.Name;
                outCol.SetDataTypeProperties(inCol.DataType, inCol.Length, inCol.Precision, inCol.Scale, inCol.CodePage);
            }

            // Add the GeoPoint column
            IDTSOutputColumn100 outColGeoPoint = ComponentMetaData.OutputCollection[0].OutputColumnCollection.New();
            outColGeoPoint.Name = OUTPUT_COLUMN_NAME_GEOPOINT;
            outColGeoPoint.SetDataTypeProperties(DataType.DT_IMAGE, 0, 0, 0, 0);

            // Add the MatchCode column
            IDTSOutputColumn100 outColMatchCode = ComponentMetaData.OutputCollection[0].OutputColumnCollection.New();
            outColMatchCode.Name = OUTPUT_COLUMN_NAME_MATCHCODE;
            outColMatchCode.SetDataTypeProperties(DataType.DT_WSTR, 10, 0, 0, 0);
        }

        /// <summary>
        /// Overrides the Validate method.  See books online for more information.
        /// </summary>
        public override DTSValidationStatus Validate()
        {
            DTSValidationStatus status = base.Validate();

            // Verify all input columns are available as output columns, since we should
            // passthru all columns of information even if they're not involved in geocoding.
            foreach (IDTSInputColumn100 inCol in InCols)
            {
                if (!OutputColumnExists(inCol.Name))
                {
                    ReinitializeMetaData();
                    break;
                }
            }

            // Verify the GeoPoint and MatchCode columns are available.
            if (!(OutputColumnExists("GeoPoint") && OutputColumnExists("MatchCode"))) { ReinitializeMetaData(); }

            // Validate that all of the Source columns have been mapped
            if (!ValidateCustomPropertySpecified(PROPERTY_NAME_INPUT_ADDRESSLINE)) return DTSValidationStatus.VS_ISBROKEN;
            if (!ValidateCustomPropertySpecified(PROPERTY_NAME_INPUT_PRIMARYCITY)) return DTSValidationStatus.VS_ISBROKEN;
            if (!ValidateCustomPropertySpecified(PROPERTY_NAME_INPUT_ENTITYID)) return DTSValidationStatus.VS_ISBROKEN;
            if (!ValidateCustomPropertySpecified(PROPERTY_NAME_INPUT_POSTALCODE)) return DTSValidationStatus.VS_ISBROKEN;

            // Validate the MapPoint settings
            if (!ValidateCustomPropertySpecified(PROPERTY_NAME_MP_USERNAME)) return DTSValidationStatus.VS_ISBROKEN;
            if (!ValidateCustomPropertySpecified(PROPERTY_NAME_MP_PASSWORD)) return DTSValidationStatus.VS_ISBROKEN;
            if (!ValidateCustomPropertySpecified(PROPERTY_NAME_MP_USESTAGING)) return DTSValidationStatus.VS_ISBROKEN;
            if (!ValidateCustomPropertySpecified(PROPERTY_NAME_MP_DATASOURCENAME)) return DTSValidationStatus.VS_ISBROKEN;
            if (!ValidateCustomPropertySpecified(PROPERTY_NAME_SERVICE_RETRYATTEMPTS)) return DTSValidationStatus.VS_ISBROKEN;
            if (!ValidateCustomPropertySpecified(PROPERTY_NAME_SERVICE_RETRYDELAY)) return DTSValidationStatus.VS_ISBROKEN;
            if (!ValidateCustomPropertySpecified(PROPERTY_NAME_SERVICE_STUBDATA)) return DTSValidationStatus.VS_ISBROKEN;

            // If all our own validations succeed, we just return the result
            // of our base classes validation.
            return status;
        }

        /// <summary>
        /// Used internally as a helper function to add custom properties to the transformation. There are three types of properties:
        /// <list type="number">
        ///     <item><term>Literal</term><description>Property whose value is based on a literal.</description></item>
        ///     <item><term>Variable</term><description>Property whose value is based on a variable.</description></item>
        ///     <item><term>InputColumn</term><description>Property whose value is based on an input column.</description></item>
        /// </list>
        /// </summary>
        /// <param name="name">Name of the property to add</param>
        /// <param name="description">Description of the property</param>
        /// <param name="defaultValue">Default value of the property</param>
        /// <param name="propertiesBase">Indicates the type of property</param>
        /// <returns>The added property</returns>
        private IDTSCustomProperty100 AddCustomProperty(string name, string description, object defaultValue, CustomPropertiesBase propertiesBase)
        {
            IDTSCustomProperty100 newProperty = ComponentMetaData.CustomPropertyCollection.New();
            newProperty.Name = name;
            newProperty.Description = description;
            newProperty.Value = defaultValue;

            if (propertiesBase == CustomPropertiesBase.Variables)
            {
                newProperty.ExpressionType = DTSCustomPropertyExpressionType.CPET_NOTIFY;
                newProperty.TypeConverter = typeof(VariablesTypeConverter).AssemblyQualifiedName;
            }
            else if (propertiesBase == CustomPropertiesBase.InputColumns)
            {
                newProperty.ExpressionType = DTSCustomPropertyExpressionType.CPET_NOTIFY;
                newProperty.TypeConverter = typeof(InputColumnsTypeConverter).AssemblyQualifiedName;
            }

            return newProperty;
        }

        /// <summary>
        /// Determines if an output column exists.
        /// </summary>
        /// <param name="columnName">Name of the output column</param>
        /// <returns>True if the column exists</returns>
        private bool OutputColumnExists(string columnName)
        {
            try
            {
                // There doesn't seem to be an Exists method, so we simulate one
                // by attempting to reference the column and catching the error.
                IDTSOutputColumn100 col = OutCols[columnName];
                return true;
            }
            catch
            {
                return false;
            }
        }

        /// <summary>
        /// Determines if a custom property has been defined in the component's metadata.
        /// </summary>
        /// <param name="propertyName">Name of the custom property</param>
        /// <returns>True if defined</returns>
        private bool ValidateCustomPropertySpecified(string propertyName)
        {
            if (String.IsNullOrEmpty(ComponentMetaData.CustomPropertyCollection[propertyName].Value.ToString().Trim()))
            {
                bool fireAgain = false;
                object[] arguments = new object[1] { DateTime.Now };
                ComponentMetaData.FireCustomEvent("Property Not Specified", "The '" + propertyName + "' was not specified.", ref arguments, "MapPoint Batch Geocoder", ref fireAgain);

                return false;
            }
            else
            {
                return true;
            }
        }
    
        /// <summary>
        /// Constructs and returns a SqlGeography datatype representing a single geo point.
        /// </summary>
        /// <param name="latitude">Latitude to use for the point</param>
        /// <param name="longitude">Longitude to use for the point</param>
        /// <returns>The SqlBytes (for ease of storing in the output buffer) of the SqlGeography.</returns>
        static private System.Data.SqlTypes.SqlBytes NewGeoPoint(string latitude, string longitude)
        {
            // First check to make sure they are valid lats and longs
            if (latitude == null || longitude == null)
                return null;

            SqlGeography geoPoint = SqlGeography.Point(double.Parse(latitude), double.Parse(longitude), MAPPOINT_SRID);

            MemoryStream stream = new MemoryStream();
            BinaryWriter writer = new BinaryWriter(stream);
            geoPoint.Write(writer);

            return new System.Data.SqlTypes.SqlBytes(stream);
        }

        /// <summary>
        /// Handles the LogVerbose event of the geocoder in order to log the event
        /// according to the package's log configuration.
        /// </summary>
        void HandleGeocoderLogVerboseEvent(object sender, LogVerboseEventArgs e)
        {
            LogVerbose(e.Message);
        }

        /// <summary>
        /// Logs a specific event to the package's log.
        /// </summary>
        private void LogVerbose(string message)
        {
            byte[] additionalData = null;
            ComponentMetaData.PostLogMessage("OnInformation", this.ComponentMetaData.Name, message, DateTime.Now, DateTime.Now, 0, ref additionalData);
        }

        #region Quick Reference Properties

        /// <summary>Quick reference to the InputCollection</summary>
        IDTSInputColumnCollection100 InCols
        { get { return ComponentMetaData.InputCollection[0].InputColumnCollection; } }

        /// <summary>Quick reference to the OutputCollection</summary>
        IDTSOutputColumnCollection100 OutCols
        { get { return ComponentMetaData.OutputCollection[0].OutputColumnCollection; } }

        /// <summary>Quick reference to the column name of the input's address line</summary>
        string SourceAddressLineColumnName
        { get { return ComponentMetaData.CustomPropertyCollection[PROPERTY_NAME_INPUT_ADDRESSLINE].Value.ToString().Trim(); } }

        /// <summary>Quick reference to the column name of the input's primary city</summary>
        string SourcePrimaryCityColumnName
        { get { return ComponentMetaData.CustomPropertyCollection[PROPERTY_NAME_INPUT_PRIMARYCITY].Value.ToString().Trim(); } }

        /// <summary>Quick reference to the column name of the input's entity id</summary>
        string SourceEntityIDColumnName
        { get { return ComponentMetaData.CustomPropertyCollection[PROPERTY_NAME_INPUT_ENTITYID].Value.ToString().Trim(); } }

        /// <summary>Quick reference to the column name of the input's postal code</summary>
        string SourcePostalCodeColumnName
        { get { return ComponentMetaData.CustomPropertyCollection[PROPERTY_NAME_INPUT_POSTALCODE].Value.ToString().Trim(); } }

        #endregion
    }
}